/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package zombiedefense.map;

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
import zombiedefense.util.MathEx;

/**
 * This class uses an array data structure to hold a collection of different types
 * of blocks in a grid. It is used to render a background/environment for games.
 * @author Laptop2010
 */
public class TileMap {

    private static Dimension tileSize = new Dimension(54, 54);
    private LinkedList<Point> spawnPoints;
    private Tile[][] tiles;
    private int defaultmapw = 50, defaultmaph = 50;
    private int mapw = defaultmapw, maph = defaultmaph;
    private Camera camera;
    private double numtilesx, numtilesy;
    private int panelw,panelh;

    public TileMap(double camerax, double cameray, int panelw, int panelh,Camera camera) {
	this.camera = camera;

	numtilesx = ((double) panelw) / (double) tileSize.width;
	numtilesy = ((double) panelh) / (double) tileSize.height;
	
	this.panelw = panelw;
	this.panelh = panelh;
	
	spawnPoints = new LinkedList<Point>();
    }

    /**
     * Will draw all tiles currently visible onscreen,
     * as indicated by the camera position.
     * @param g 
     */
    public void draw(Graphics2D g) {
	int x = (int) camera.x;
	int tileStartY = (int)camera.y;
	int y = tileStartY;
	int screenStartY = getScreenY(tileStartY);
	int panelx = getScreenX(x);
	int panely = screenStartY;

	while(true) {
	    while(true) {

		if (panelx < -tileSize.width) {
		    break; //this tile is offscreen - move to next column
		} else if (x >= mapw) {
		    return; //have finished all onscreen tiles
		}

		if (panely < -tileSize.height) {
		    continue; //this tile is offscreen
		} else if (y >= maph) {
		    break; //start next column of tiles; have reach end of this one
		}

		//if bounding conditions succeed, draw
		g.drawImage(getTileType(x, y).getImage(),panelx,panely, null);
		
		panely += tileSize.height;
		y++;
	    }
	    x++;    //move to next column
	    y = (int)camera.y; //reset tile y
	    
	    panelx += tileSize.width;  
	    panely = screenStartY;
	}
    }

    public void update(long elapsedTime) {
	camera.update(elapsedTime);
	
	//camera bounding conditions - had to be tweaked because of cast-related error
	if (camera.x < 0) {
	    camera.x = 0;
	    camera.dx = 0;
	} else if (camera.x + numtilesx >= mapw) {
	    camera.x = mapw - numtilesx;
	    camera.dx = 0;
	}

	if (camera.y < 0) {
	    camera.y = 0;
	    camera.dy = 0;
	} else if (camera.y + numtilesy >= maph) {
	    camera.y = maph - numtilesy;
	    camera.dy = 0;
	}
    }

    public int getTileWidth() {
	return tileSize.width;
    }

    public int getTileHeight() {
	return tileSize.height;
    }
    
    public Camera getCamera(){
	return camera;
    }
    
    /**
     * Centers the camera over the player.
     */
    public void centerCamera(){
	camera.center(panelw,panelh);
    }

    public int getScreenX(double gridx) {
	return (int) ((gridx - camera.x) * (double) tileSize.width);
    }

    public int getScreenY(double gridy) {
	return (int) ((gridy - camera.y) * (double) tileSize.height);
    }

    public double getGridX(int screenx) {
	return ((double) screenx) / ((double) tileSize.width) + camera.x;
    }

    public double getGridY(int screeny) {
	return ((double) screeny) / ((double) tileSize.height) + camera.y;
    }

    public static double pixelsToGridUnits(int pixels) {
	return (double) pixels / (double) tileSize.width;
    }

    public static int gridUnitsToPixels(double grid) {
	return (int) (grid * tileSize.width);
    }

    public boolean isSolid(double x, double y) {
	return getTileType(x, y).isSolid();
    }

    public boolean isSolid(int x, int y) {
	return getTileType(x, y).isSolid();
    }

    /**
     * Get a tile at a certain location.
     * @param x
     * @param y
     * @return 
     */
    public TileType getTileType(double x, double y) {
	int ix = (int) x;
	int iy = (int) y;

	return getTileType(ix, iy);
    }

    /**
     * Get a tile at a certain location.
     * Offscreen tiles are treated as solid (brick).
     * @param x
     * @param y
     * @return 
     */
    public TileType getTileType(int x, int y) {

	if (x < 0 || y < 0 || x >= mapw || y >= maph) {
	    return TileType.graybrick;
	}

	return tiles[x][y].getType();
    }

    /**
     * Testing: map is generated in code.
     * dirt = d
     * grass = g
     * metal = m
     * brick = b
     * wood = w
     */
    private void generateMap() {
	mapw = defaultmapw;
	maph = defaultmaph;

	tiles = new Tile[mapw][maph];

	for (int x = 0; x < mapw; x++) {
	    for (int y = 0; y < maph; y++) {
		tiles[x][y] = getRandomTile(x, y);
	    }
	}
    }

    //returns a random tile type out of all possible types
    private Tile getRandomTile(int x, int y) {
	TileType[] vals = TileType.values();
	return new Tile(x, y, vals[MathEx.getRandomInt(0, vals.length - 1)]);
    }

    private TileType getTileType(char c) {
	for (TileType t : TileType.values()) {
	    if (t.getChar() == c) {
		return t;
	    }
	}

	throw new IllegalArgumentException("bad tile map: " + "\"" + c + "\"");
    }

    public Tile getTile(double x, double y) {
	return getTile((int)x,(int)y);
    }

    public Tile getTile(int x, int y) {
	//avoid exceptions
	//note: object creation here
	if (x < 0 || x >= mapw || y < 0 || y >= maph) {
	    return null;
	} else {
	    return tiles[x][y];
	}
    }

    /**
     * Returns the nearest passable tile object
     * to the specified coordinates. NOTE: only
     * checks one tile in each of the four directions:
     * n,s,e,w.
     * @param x
     * @param y
     * @return 
     */
    public Tile getNearestPassableTile(double x, double y) {
	Tile t = getTile(x, y);
	
	//do not run algorithm on tiles that are already passable
	if(t != null){
	    if(!t.getType().isSolid()){
		return t;
	    }
	}

	double fx = getFractionalPart(x);
	double fy = getFractionalPart(y);
	
	//check x preference
	if (fx > .5) {
	    t = getTile(x + 1, y);
	} else {
	    t = getTile(x - 1, y);
	}

	if (t != null && !t.getType().isSolid()) {
	    return t;
	}

	//check y preference
	if (fy > .5) {
	    t = getTile(x, y + 1);
	} else {
	    t = getTile(x, y - 1);
	}

	if (t != null && !t.getType().isSolid()) {
	    return t;
	}

	return null; //no tile found
    }
    
    /**
     * Gets the nearest passable tile (within 5 tiles or null is
     * returned) to the specified tile. NOTE: only checks horizontally
     * and vertically
     * @param x
     * @param y
     * @return 
     */
    public Tile getNearestPassableTile(int x,int y){
	int adjustment = 1;
	int maxTries = 5;
	Tile t;
	
	while(maxTries > 0){
	    t = getTile(x + adjustment,y);
	    if(t != null && !t.getType().isSolid()){
		return t;
	    }
	    
	    t = getTile(x - adjustment,y);
	    if(t != null && !t.getType().isSolid()){
		return t;
	    }
	    
	    t = getTile(x,y + adjustment);
	    if(t != null && !t.getType().isSolid()){
		return t;
	    }
	    
	    t = getTile(x,y - adjustment);
	    if(t != null && !t.getType().isSolid()){
		return t;
	    }
	    
	    maxTries--;
	    adjustment++;
	}
	return null;
	
    }

    private double getFractionalPart(double d) {
	if (d > 0) {
	    return d - ((int) d);
	} else if (d < 0) {
	    return d + ((int) d);
	} else {
	    return 0.0;
	}
    }

    /**
     * Resets the list values of each tile to
     * Node.ListValue.None. Used by pathfinding
     * routines to prepare for the next path find.
     */
    public void resetTiles() {
	for (int x = 0; x < mapw; x++) {
	    for (int y = 0; y < maph; y++) {
		tiles[x][y].setListValue(Node.ListValue.None);
	    }
	}
    }

    public void load(String filename) {
	try {
	    Scanner s = new Scanner(getClass().getResourceAsStream("/maps/" + filename));
	    if (s.hasNext()) {
		mapw = s.nextInt();
		maph = s.nextInt();

		if (mapw < numtilesx || maph < numtilesy) {
		    throw new Exception();
		}
	    }

	    //initialize tile array
	    tiles = new Tile[mapw][maph];

	    //read in tiles one line at a time
	    char[] lineBuf = new char[mapw];
	    String line;
	    int linenum = 0;

	    //read all lines of map data
	    while (s.hasNextLine() && linenum < maph) {
		line = s.nextLine();

		//skip blank lines
		if (line.length() <= 0) {
		    continue;
		}

		//read the chars
		for (int i = 0; i < mapw; i++) {
		    lineBuf[i] = line.charAt(i);
		}

		//translate into Tile objects
		loadLine(lineBuf, linenum);

		linenum++;
	    }
	    
	    //read all spawn-points
	    while(s.hasNext()){
		Point p = new Point(s.nextInt(),s.nextInt());
		Tile t = getNearestPassableTile(p.x,p.y);
		p.x = t.getX();
		p.y = t.getY();
		spawnPoints.add(p);
	    }

	    s.close();	//close file

	} catch (Exception e) {
	    e.printStackTrace();
	    System.out.println("map loading failed: generating random map");

	    //generate a random map to at least give player something to play on
	    generateMap();
	}
    }

    private void loadLine(char[] buffer, int linenum) {
	for (int i = 0; i < buffer.length; i++) {
	    tiles[i][linenum] = new Tile(i, linenum, getTileType(buffer[i]));
	}
    }
    
    public List<Point> getSpawnPoints(){
	return spawnPoints;
    }
}
